import websocket
import threading
from decimal import Decimal
import traceback
import ssl
from time import sleep
import json
import decimal
from . bitmex_auth import *
from future.standard_library import hooks

with hooks():  # Python 2/3 compat
    from urllib.parse import urlparse, urlunparse

import logging

logger = logging.getLogger('root')
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
# create formatter
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# add formatter to ch
ch.setFormatter(formatter)
logger.addHandler(ch)


def findItemByKeys(keys, table, matchData):
    for item in table:
        matched = True
        for key in keys:
            if item[key] != matchData[key]:
                matched = False
        if matched:
            return item


def toNearest(num, tickSize):
    """Given a number, round it to the nearest tick. Very useful for sussing float error
       out of numbers: e.g. toNearest(401.46, 0.01) -> 401.46, whereas processing is
       normally with floats would give you 401.46000000000004.
       Use this after adding/subtracting/multiplying numbers."""
    tickDec = Decimal(str(tickSize))
    return float((Decimal(round(num / tickSize, 0)) * tickDec))


# Connects to BitMEX websocket for streaming realtime data.
# The Marketmaker still interacts with this as if it were a REST Endpoint, but now it can get
# much more realtime data without heavily polling the API.
#
# The Websocket offers a bunch of data as raw properties right on the object.
# On connect, it synchronously asks for a push of all this data then returns.
# Right after, the MM can start using its data. It will be updated in realtime, so the MM can
# poll as often as it wants.

# websocket
WS_BASE_URL = 'wss://www.bitmex.com/realtime/'
WS_BASE_TEST_URL = 'wss://testnet.bitmex.com/realtime/'


# ===========订阅topic说明===========
# "announcement", // 网站公告
# "chat", // Trollbox聊天室
# "connected", // 已连接用户 / 机器人的统计数据
# "funding", // 掉期产品的资金费率更新每个资金时段发送（通常是8小时）
# "instrument", // 产品更新，包括交易量以及报价
# "insurance", // 每日保险基金的更新
# "liquidation", // 进入委托列表的强平委托
# "orderBookL2", // 完整的level2委托列表
# "orderBook10", // 前10层的委托列表，用传统的完整委托列表推送
# "publicNotifications", // 全系统的告示（用于段时间的消息）
# "quote", // 最高层的委托列表
# "quoteBin1m", // 每分钟报价数据
# "quoteBin5m", // 每5分钟报价数据
# "quoteBin1h", // 每个小时报价数据
# "quoteBin1d", // 每天报价数据
# "settlement", // 结算信息
# "trade", // 实时交易
# "tradeBin1m", // 每分钟交易数据
# "tradeBin5m", // 每5分钟交易数据
# "tradeBin1h", // 每小时交易数据
# "tradeBin1d", // 每天交易数据
# "execution", // 个别成交，可能是多个成交
# "order", // 你委托的更新
# "margin", // 你账户的余额和保证金要求的更新
# "position", // 你仓位的更新
# "privateNotifications", // 个人的通知，现时并未使用
# "transact" // 资金提存更新
# "wallet" // 比特币余额更新及总提款存款


class BitMEXWebsocket(object):

    def __init__(self, API_KEY, API_SECRET, test=False, sub_account_msg=False, use_proxy=True):

        super(BitMEXWebsocket, self).__init__()
        self.keep_run = False
        self.logger = logger
        self.__reset()
        self.API_KEY = API_KEY
        self.API_SECRET = API_SECRET
        self.is_test = test
        self.is_reconnect = False
        self.on_msg_callback = []
        self.sub_account_msg = sub_account_msg # 是否订阅用户信息
        self.use_proxy = use_proxy
        # 用户相关的信息
        self.subscriptions = ["margin", "execution", "funding", "transact", "position", "order"]

    def set_on_msg_callbacks(self, callback):
        self.on_msg_callback.append(callback)

    def __del__(self):
        self.exit()

    def connect(self):
        '''Connect to the websocket and initialize data stores.'''
        self.keep_run = True
        if self.is_test:
            endpoint = WS_BASE_TEST_URL
        else:
            endpoint = WS_BASE_URL

        # Get WS URL and connect.
        urlParts = list(urlparse(endpoint))
        urlParts[0] = urlParts[0].replace('http', 'ws')
        if self.sub_account_msg:
            urlParts[2] = "/realtime?subscribe=" + ",".join(self.subscriptions)
        else:
            urlParts[2] = "/realtime"
        wsURL = urlunparse(urlParts)
        self.logger.info("Connecting to %s" % wsURL)
        self.__connect(wsURL)

    def sub_account_msg(self):
        self.__send_command('subscribe', self.subscriptions)

    def sub_topic(self, topic_name):
        if not topic_name in self.subscriptions:
            self.__send_command('subscribe',[topic_name,])
            self.subscriptions.append(topic_name)

    def sub_symbol(self, symbol):
        self.__send_command('subscribe', [sub + ":" + symbol for sub in ['orderBook10', 'trade']])
        self.subed_symbol.add(symbol)

    def unsub_symbol(self, symbol):
        if symbol in self.subed_symbol:
            self.__send_command('unsubscribe', [sub + ":" + symbol for sub in ['orderBook10', 'trade']])
            self.subed_symbol.remove(symbol)

    def error(self, err):
        self._error = err
        self.logger.error(err)
        if self.keep_run and not self.ws.sock.connected:
            self.__reconnect()

    def __reconnect(self):
        if self.ws.sock.connected:
            self.ws.close()
        sleep(3)
        self.ws = None
        self.connect()
        self.is_reconnect = True

    def exit(self):
        self.keep_run = False
        if self.ws:
            self.ws.close()

    def __connect(self, wsURL):
        '''Connect to the websocket in a thread.'''
        self.logger.debug("Starting thread")

        self.ws = websocket.WebSocketApp(wsURL,
                                         on_message=self.__on_message,
                                         on_close=self.__on_close,
                                         on_open=self.__on_open,
                                         on_error=self.__on_error,
                                         header=self.__get_auth(),
                                         )
        self.wst = threading.Thread(target=self.__run_ws)
        self.wst.daemon = True
        self.wst.start()
        self.logger.info("Started thread")

    def __run_ws(self):
        if self.keep_run:
            ssl_defaults = ssl.get_default_verify_paths()
            sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
            if self.use_proxy:
                self.ws.run_forever(sslopt=sslopt_ca_certs, ping_interval=30,
                                    http_proxy_host="127.0.0.1",
                                    http_proxy_port=1080,)
            else:
                self.ws.run_forever(sslopt=sslopt_ca_certs, ping_interval=30)

    def __get_auth(self):
        '''Return auth headers. Will use API Keys if present in settings.'''
        self.logger.info("Authenticating with API Key.")
        # To auth to the WS using an API key, we generate a signature of a nonce and
        # the WS API endpoint.
        nonce = generate_nonce()
        return [
            "api-nonce: " + str(nonce),
            "api-signature: " + generate_signature(self.API_SECRET, 'GET', '/realtime', nonce, ''),
            "api-key:" + self.API_KEY
        ]

    def __send_command(self, command, args):
        '''Send a raw command.'''
        self.ws.send(json.dumps({"op": command, "args": args or []}))

    def __on_message(self, ws, message):
        '''Handler for parsing WS messages.'''
        # self.logger.debug(json.dumps(message))
        # 登陆成功之后的欢迎信息
        # {'info': 'Welcome to the BitMEX Realtime API.',
        # 'version': '2018-10-23T18:33:47.000Z',
        # 'timestamp': '2018-10-25T03:28:33.796Z',
        # 'docs': 'https://www.bitmex.com/app/wsAPI', 'limit': {'remaining': 39}}
        json_msg = json.loads(message)
        remaining_limit = json_msg["limit"]["remaining"] if "limit" in json_msg else None
        if remaining_limit:
            logger.info(f"{json_msg['timestamp']} : 当前bitmex 剩余websocket连接数 {remaining_limit}")

        try:
            if 'subscribe' in json_msg:
                if json_msg['success']:
                    self.logger.debug("Subscribed to %s." % json_msg['subscribe'])
                else:
                    self.error("Unable to subscribe to %s. Error: \"%s\" Please check and restart." %
                               (json_msg['request']['args'][0], json_msg['error']))
            elif 'status' in json_msg:
                if json_msg['status'] == 400:
                    self.error(json_msg['error'])
                if json_msg['status'] == 401:
                    self.error("API Key incorrect, please check and restart.")
            for callback in self.on_msg_callback:
                callback(message)
        except:
            self.logger.error(traceback.format_exc())

    def __on_open(self, ws):
        self.logger.debug("Websocket Opened.")
        if self.is_reconnect:
            for symbol in self.subed_symbol:
                self.sub_symbol(symbol)

    def __on_close(self, ws):
        self.logger.info('Websocket Closed')
        if self.keep_run:
            self.__reconnect()

    def __on_error(self, ws, error):
        self.error(error)

    def __reset(self):
        self.data = {}
        self.keys = {}
        self.subed_symbol = set([])
        self.exited = False
        self._error = None

